/*
 * Copyright 2010 EES GmbH
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * 	http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.eesgmbh.gimv.samples.googlechartapi.client;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.SortedMap;

import org.eesgmbh.gimv.client.event.LoadImageDataEvent;
import org.eesgmbh.gimv.client.event.LoadImageDataEventHandler;
import org.eesgmbh.gimv.client.event.SetDataAreaPixelBoundsEvent;
import org.eesgmbh.gimv.client.event.SetDomainBoundsEvent;
import org.eesgmbh.gimv.client.event.SetDomainBoundsEventHandler;
import org.eesgmbh.gimv.client.event.SetImageUrlEvent;
import org.eesgmbh.gimv.client.event.SetMaxDomainBoundsEvent;
import org.eesgmbh.gimv.client.event.SetViewportPixelBoundsEvent;
import org.eesgmbh.gimv.client.history.DefaultSetDomainBoundsEventHistoryTokenTransformer;
import org.eesgmbh.gimv.client.history.UnparsableHistoryTokenException;
import org.eesgmbh.gimv.client.widgets.Viewport;
import org.eesgmbh.gimv.samples.googlechartapi.client.data.SampleDataFactory;
import org.eesgmbh.gimv.samples.googlechartapi.client.linecolors.ChangeLineColorsEvent;
import org.eesgmbh.gimv.samples.googlechartapi.client.linecolors.ChangeLineColorsEventHandler;
import org.eesgmbh.gimv.shared.util.Bounds;
import org.eesgmbh.gimv.shared.util.DateUtils;

import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.HandlerManager;
import com.google.gwt.user.client.History;
import com.google.gwt.user.client.Window;
import com.googlecode.charts4j.AxisLabels;
import com.googlecode.charts4j.AxisLabelsFactory;
import com.googlecode.charts4j.AxisStyle;
import com.googlecode.charts4j.AxisTextAlignment;
import com.googlecode.charts4j.Color;
import com.googlecode.charts4j.Data;
import com.googlecode.charts4j.DataUtil;
import com.googlecode.charts4j.Fills;
import com.googlecode.charts4j.GCharts;
import com.googlecode.charts4j.Line;
import com.googlecode.charts4j.LineChart;
import com.googlecode.charts4j.LinearGradientFill;
import com.googlecode.charts4j.Plots;

/**
 *
 * @author Sascha Hagedorn - EES GmbH - s.hagedorn@ees-gmbh.de
 *
 */
public class GoogleChartApiSampleController {

	private final SortedMap<Date, Integer> SAMPLE_DATA1;
	private final SortedMap<Date, Integer> SAMPLE_DATA2;

	private final Viewport viewport;
	private final HandlerManager handlerManager;

	private final HistoryManager historyManager;

	/*
	 * Memorize the bounds of what is currently displayed. That is
	 * time range and value range
	 */
	private Bounds currentDomainBounds;

	private HashMap<Integer, String> lineColors;

	public GoogleChartApiSampleController(Viewport viewport, HandlerManager handlerManager) {
		this.viewport = viewport;
		this.handlerManager = handlerManager;

		this.historyManager = new HistoryManager();

		//register to receive the following events
		GoogleChartApiSampleEventHandler eventHandler = new GoogleChartApiSampleEventHandler();
		handlerManager.addHandler(LoadImageDataEvent.TYPE, eventHandler);
		handlerManager.addHandler(SetDomainBoundsEvent.TYPE, eventHandler);
		handlerManager.addHandler(ChangeLineColorsEvent.TYPE, eventHandler);

		//manage history
		History.addValueChangeHandler(this.historyManager);

		//setup the sample data
		SAMPLE_DATA1 = SampleDataFactory.getSampleData1();
		SAMPLE_DATA2 = SampleDataFactory.getSampleData2();

		//and prepare the custom line color changing feature
		lineColors = new HashMap<Integer, String>();
		lineColors.put(1, "86AC32");
		lineColors.put(2, "836AE6");
	}

	/**
	 * Must only be invoked after Gimv components are all set up.
	 */
	public void init() {
		//inform everyone of the viewport bounds (that is the dimensions of the displayed image in pixel)
		handlerManager.fireEvent(new SetViewportPixelBoundsEvent(getViewportBounds()));

		//tell everyone about the section within the image where the chart is actually displayed (without the
		//space occupied by the axes or padding)
		handlerManager.fireEvent(new SetDataAreaPixelBoundsEvent(new Bounds(27, 533, 6, 295)));

		//the maximum time period in which data is available
		//note that a maximum value range is not defined here
		handlerManager.fireEvent(new SetMaxDomainBoundsEvent(new Bounds(
				DateUtils.truncateTime(SAMPLE_DATA1.firstKey()).getTime(),
				DateUtils.ceilTime(SAMPLE_DATA1.lastKey()).getTime(), null, null)));


		//Reconstruct state from browser history token
		if (!"".equals(History.getToken())) { //has history items
			History.fireCurrentHistoryState();


		//Or Initialize to some defaults
		} else {
			List<Date> dataSetDates = sampleDataSetDatesAsList();
			Bounds sampleDataSetValueRange = sampleDataSetValueRange();

			//tell everyone about the initial domain bounds (that is time range horizontally and value range vertically)
			currentDomainBounds = new Bounds(
					(double) dataSetDates.get((int) (dataSetDates.size() * 0.3d)).getTime(),
					(double) dataSetDates.get((int) (dataSetDates.size() * 0.7d)).getTime(),
					sampleDataSetValueRange.getTop(),
					sampleDataSetValueRange.getBottom()
			);
			handlerManager.fireEvent(new SetDomainBoundsEvent(currentDomainBounds));

			//get everything started by firing the LoadImageDataEvent, we will end up in the onLoadImageData method
			handlerManager.fireEvent(new LoadImageDataEvent());
		}
	}

	/**
	 * Invoked when a new image with new properties must be retrieved from the google chart api
	 */
	private void onLoadImageData(LoadImageDataEvent event) {
		//create the new google chart api URL
		String url = createUrl(currentDomainBounds);

		//and firing the event with the url
		handlerManager.fireEvent(new SetImageUrlEvent(url));

		//Update the browser history token
		History.newItem("bounds=" + DefaultSetDomainBoundsEventHistoryTokenTransformer.toHistoryTokenValue(new SetDomainBoundsEvent(currentDomainBounds)), false);
	}

	/**
	 * Invoked when someone changed the section of what was displayed
	 */
	private void onSetDomainBounds(SetDomainBoundsEvent event) {
		currentDomainBounds = event.getBounds();
	}

	/**
	 * Custom event when the line color of one of two charts should change
	 */
	private void onChangeLineColors(ChangeLineColorsEvent event) {
		lineColors.put(event.getIndex(), event.getColorCode());

		handlerManager.fireEvent(new SetImageUrlEvent(createUrl(currentDomainBounds)));
	}

	/**
	 * Creates the Google Chart Api URL with the help of the charts4j
	 * project (http://code.google.com/p/charts4j/).
	 */
	private String createUrl(Bounds bounds) {
		// Scaling the data to Gcharts format
		Data data1 = getScaledAndRangedData(bounds, SAMPLE_DATA1);
		Data data2 = getScaledAndRangedData(bounds, SAMPLE_DATA2);

		// the two graphs
		Line line1 = Plots.newLine(data1, Color.newColor(lineColors.get(1)));
		Line line2 = Plots.newLine(data2, Color.newColor(lineColors.get(2)));

		// find the intersection
		int index = -1;
		for (int i = 0; i < data1.getSize(); i++) {
			if (Math.abs(data1.getData()[i] - data2.getData()[i]) == 0) {
				index = i;
			}
		}

		// setup graphs and grid
		LineChart chart= GCharts.newLineChart(line1, line2);
		chart.setSize(getViewportBounds().getAbsWidth().intValue(), getViewportBounds().getAbsHeight().intValue());
		chart.setGrid(100, 10, 3, 3);

		LinearGradientFill fill = Fills.newLinearGradientFill(90, Color.newColor("EAEDF2"), 50);
		fill.addColorAndOffset(Color.newColor("9BD8F5"), 0);
		chart.setAreaFill(fill);
		chart.setBackgroundFill(Fills.newSolidFill(Color.newColor("EAEDF2")));

		// insert marker if intersection is available
		if (index > -1) {
			double pos = (double)index / data1.getSize()*100;
			chart.addVerticalRangeMarker(Math.max(pos - 4.0, 0.0), Math.min(pos + 4.0, 100.0), Color.newColor("E6584D", 40));
		}

		// insert Labels
		AxisLabels yAxisLabels = AxisLabelsFactory.newNumericRangeAxisLabels(bounds.getBottom(), bounds.getTop());
		yAxisLabels.setAxisStyle(AxisStyle.newAxisStyle(Color.BLACK, 12, AxisTextAlignment.CENTER));
		chart.addYAxisLabels(yAxisLabels);

		// get the URL for the image
		return chart.toURLString();
	}

	private Bounds getViewportBounds() {
		return new Bounds(0, viewport.getOffsetWidth(), 0, viewport.getOffsetHeight());
	}

	private List<Date> sampleDataSetDatesAsList() {
		return new ArrayList<Date>(SAMPLE_DATA1.keySet());
	}

	private Bounds sampleDataSetValueRange() {
		return new Bounds(null, null, SampleDataFactory.getMaxValue(), SampleDataFactory.getMinValue());
	}

	private Data getScaledAndRangedData(Bounds bounds, SortedMap<Date, Integer> dataSet) {
		ArrayList<Integer> data = new ArrayList<Integer>(dataSet.subMap(new Date(bounds.getLeft().longValue()), new Date(bounds.getRight().longValue())).values());
		return DataUtil.scaleWithinRange(bounds.getBottom(), bounds.getTop(), data);
	}

	/**
	 * A dummy delegation class for all received events
	 */
	private class GoogleChartApiSampleEventHandler implements LoadImageDataEventHandler, SetDomainBoundsEventHandler, ChangeLineColorsEventHandler {

		public void onSetDomainBounds(SetDomainBoundsEvent event) {
			GoogleChartApiSampleController.this.onSetDomainBounds(event);
		}

		public void onLoadImageData(LoadImageDataEvent event) {
			GoogleChartApiSampleController.this.onLoadImageData(event);
		}

		public void onChangeLineColors(ChangeLineColorsEvent event) {
			GoogleChartApiSampleController.this.onChangeLineColors(event);
		}
	}

	/**
	 * Well, manages the browser history token.
	 */
	private class HistoryManager implements ValueChangeHandler<String> {

		public void onValueChange(ValueChangeEvent<String> event) {

			if (event.getValue().contains("bounds=")) {
				String boundsHistoryTokenValue = event.getValue().substring(event.getValue().indexOf("bounds=") + "bounds=".length());

				try {
					handlerManager.fireEvent(DefaultSetDomainBoundsEventHistoryTokenTransformer.toEvent(boundsHistoryTokenValue));
					handlerManager.fireEvent(new LoadImageDataEvent());
				} catch (UnparsableHistoryTokenException e) {
					e.printStackTrace();

					Window.alert(e.getMessage());
				}
			}
		}
	}
}

